/*
 * Copyright (C) 2015. Jared Rummler <jared.rummler@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.tronsis.library.util.ProcessesUtil;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;

import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

public class AndroidAppProcess extends AndroidProcess {

  private static final boolean SYS_SUPPORTS_SCHEDGROUPS = new File("/dev/cpuctl/tasks").exists();

  // The name may contain uppercase or lowercase letters ('A' through 'Z'), numbers, and underscores ('_').
  // However, individual package name parts may only start with letters.
  // A process name can contain a colon.
  private static final Pattern PROCESS_NAME_PATTERN = Pattern.compile(
      "^([A-Za-z]{1}[A-Za-z0-9_]*[\\.|:])*[A-Za-z][A-Za-z0-9_]*$");

  /** {@code true} if the process is in the foreground */
  public final boolean foreground;

  /** The user id of this process. */
  public final int uid;

  public AndroidAppProcess(int pid) throws IOException, NotAndroidAppProcessException {
    super(pid);
    if (name == null || !PROCESS_NAME_PATTERN.matcher(name).matches() ||
        !new File("/data/data", getPackageName()).exists()) {
      throw new NotAndroidAppProcessException(pid);
    }

    final boolean foreground;
    int uid;

    if (SYS_SUPPORTS_SCHEDGROUPS) {
      Cgroup cgroup = cgroup();
      ControlGroup cpuacct = cgroup.getGroup("cpuacct");
      ControlGroup cpu = cgroup.getGroup("cpu");
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        if (cpu == null || cpuacct == null || !cpuacct.group.contains("pid_")) {
          throw new NotAndroidAppProcessException(pid);
        }
        foreground = !cpu.group.contains("bg_non_interactive");
        try {
          uid = Integer.parseInt(cpuacct.group.split("/")[1].replace("uid_", ""));
        } catch (Exception e) {
          uid = status().getUid();
        }
        AndroidProcesses.log("name=%s, pid=%d, uid=%d, foreground=%b, cpuacct=%s, cpu=%s",
            name, pid, uid, foreground, cpuacct.toString(), cpu.toString());
      } else {
        if (cpu == null || cpuacct == null || !cpu.group.contains("apps")) {
          throw new NotAndroidAppProcessException(pid);
        }
        foreground = !cpu.group.contains("bg_non_interactive");
        try {
          uid = Integer.parseInt(cpuacct.group.substring(cpuacct.group.lastIndexOf("/") + 1));
        } catch (Exception e) {
          uid = status().getUid();
        }
        AndroidProcesses.log("name=%s, pid=%d, uid=%d foreground=%b, cpuacct=%s, cpu=%s",
            name, pid, uid, foreground, cpuacct.toString(), cpu.toString());
      }
    } else {
      Stat stat = stat();
      Status status = status();
      // https://github.com/android/platform_system_core/blob/jb-mr1-release/libcutils/sched_policy.c#L245-256
      foreground = stat.policy() == 0; // SCHED_NORMAL
      uid = status.getUid();
      AndroidProcesses.log("name=%s, pid=%d, uid=%d foreground=%b", name, pid, uid, foreground);
    }

    this.foreground = foreground;
    this.uid = uid;
  }

  protected AndroidAppProcess(Parcel in) {
    super(in);
    foreground = in.readByte() != 0x00;
    uid = in.readInt();
  }

  /**
   * @return the app's package name
   * @see #name
   */
  public String getPackageName() {
    return name.split(":")[0];
  }

  /**
   * Retrieve overall information about the application package.
   *
   * <p>Throws {@link PackageManager.NameNotFoundException} if a package with the given name can
   * not be found on the system.</p>
   *
   * @param context
   *     the application context
   * @param flags
   *     Additional option flags. Use any combination of
   *     {@link PackageManager#GET_ACTIVITIES}, {@link PackageManager#GET_GIDS},
   *     {@link PackageManager#GET_CONFIGURATIONS}, {@link PackageManager#GET_INSTRUMENTATION},
   *     {@link PackageManager#GET_PERMISSIONS}, {@link PackageManager#GET_PROVIDERS},
   *     {@link PackageManager#GET_RECEIVERS}, {@link PackageManager#GET_SERVICES},
   *     {@link PackageManager#GET_SIGNATURES}, {@link PackageManager#GET_UNINSTALLED_PACKAGES}
   *     to modify the data returned.
   * @return a PackageInfo object containing information about the package.
   */
  public PackageInfo getPackageInfo(Context context, int flags)
      throws PackageManager.NameNotFoundException {
    return context.getPackageManager().getPackageInfo(getPackageName(), flags);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    super.writeToParcel(dest, flags);
    dest.writeByte((byte) (foreground ? 0x01 : 0x00));
    dest.writeInt(uid);
  }

  public static final Parcelable.Creator<AndroidAppProcess> CREATOR = new Parcelable.Creator<AndroidAppProcess>() {

    @Override
    public AndroidAppProcess createFromParcel(Parcel source) {
      return new AndroidAppProcess(source);
    }

    @Override
    public AndroidAppProcess[] newArray(int size) {
      return new AndroidAppProcess[size];
    }
  };

  public static final class NotAndroidAppProcessException extends Exception {

    public NotAndroidAppProcessException(int pid) {
      super(String.format("The process %d does not belong to any application", pid));
    }
  }

}
